/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.auditing.MappingAuditableBeanWrapperFactory;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.geo.GeoModule;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.querydsl.QuerydslUtils;
import org.springframework.data.querydsl.binding.QuerydslBindingsFactory;
import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder;
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.MetadataConfiguration;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.event.AnnotatedEventHandlerInvoker;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
import org.springframework.data.rest.core.support.DomainObjectMerger;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.core.support.RepositoryRelProvider;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory;
import org.springframework.data.rest.core.util.Java8PluginRegistry;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping;
import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
import org.springframework.data.rest.webmvc.HttpHeadersPreparer;
import org.springframework.data.rest.webmvc.ProfileResourceProcessor;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestExceptionHandler;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping;
import org.springframework.data.rest.webmvc.RestMediaTypes;
import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver;
import org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter;
import org.springframework.data.rest.webmvc.alps.RootResourceInformationToAlpsDescriptorConverter;
import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter;
import org.springframework.data.rest.webmvc.json.DomainObjectReader;
import org.springframework.data.rest.webmvc.json.EnumTranslator;
import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper;
import org.springframework.data.rest.webmvc.json.JacksonMappingAwareSortTranslator;
import org.springframework.data.rest.webmvc.json.JacksonSerializers;
import org.springframework.data.rest.webmvc.json.MappingAwareDefaultedPageableArgumentResolver;
import org.springframework.data.rest.webmvc.json.MappingAwarePageableArgumentResolver;
import org.springframework.data.rest.webmvc.json.MappingAwareSortArgumentResolver;
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer;
import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter;
import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter.ValueTypeSchemaPropertyCustomizerFactory;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.support.DefaultExcerptProjector;
import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping;
import org.springframework.data.rest.webmvc.support.DomainClassResolver;
import org.springframework.data.rest.webmvc.support.ETagArgumentResolver;
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.support.JpaHelper;
import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.data.util.AnnotatedTypeScanner;
import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver;
import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration;
import org.springframework.data.web.config.SpringDataJacksonConfiguration;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.mvc.ResourceProcessorInvoker;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
/**
* Main application configuration for Spring Data REST. To customize how the exporter works, subclass this and override
* any of the {@literal configure*} methods.
* <p/>
* Any XML files located in the classpath under the {@literal META-INF/spring-data-rest/} path will be automatically
* found and loaded into this {@link org.springframework.context.ApplicationContext}.
*
* @author Oliver Gierke
* @author Jon Brisbin
* @author Greg Turnquist
* @author Mark Paluch
*/
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL)
@ComponentScan(basePackageClasses = RepositoryRestController.class,
includeFilters = @Filter(BasePathAwareController.class), useDefaultFilters = false)
@ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml")
@Import({ SpringDataJacksonConfiguration.class, EnableSpringDataWebSupport.QuerydslActivator.class })
public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration implements InitializingBean {
private static final boolean IS_JPA_AVAILABLE = ClassUtils.isPresent("javax.persistence.EntityManager",
RepositoryRestMvcConfiguration.class.getClassLoader());
@Autowired ApplicationContext applicationContext;
@Autowired(required = false) List<BackendIdConverter> idConverters = Collections.emptyList();
@Autowired(required = false) List<RepositoryRestConfigurer> configurers = Collections.emptyList();
@Autowired(required = false) List<EntityLookup<?>> lookups = Collections.emptyList();
@Autowired(required = false) RelProvider relProvider;
@Autowired(required = false) CurieProvider curieProvider;
private RepositoryRestConfigurerDelegate configurerDelegate;
public RepositoryRestMvcConfiguration(ApplicationContext context,
@Qualifier("mvcConversionService") ObjectFactory<ConversionService> conversionService) {
super(context, conversionService);
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
this.configurerDelegate = new RepositoryRestConfigurerDelegate(configurers);
}
@Bean
public Repositories repositories() {
return new Repositories(applicationContext);
}
@Bean
public RepositoryRelProvider repositoryRelProvider(ObjectFactory<ResourceMappings> resourceMappings) {
return new RepositoryRelProvider(resourceMappings);
}
@Bean
public PersistentEntities persistentEntities() {
List<MappingContext<?, ?>> arrayList = new ArrayList<MappingContext<?, ?>>();
for (MappingContext<?, ?> context : BeanFactoryUtils
.beansOfTypeIncludingAncestors(applicationContext, MappingContext.class).values()) {
arrayList.add(context);
}
return new PersistentEntities(arrayList);
}
@Bean
@Qualifier
public DefaultFormattingConversionService defaultConversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
// Add Spring Data Commons formatters
conversionService.addConverter(uriToEntityConverter(conversionService));
addFormatters(conversionService);
configurerDelegate.configureConversionService(conversionService);
return conversionService;
}
/**
* {@link org.springframework.context.ApplicationListener} implementation for invoking
* {@link org.springframework.validation.Validator} instances assigned to specific domain types.
*/
@Bean
public ValidatingRepositoryEventListener validatingRepositoryEventListener(
ObjectFactory<PersistentEntities> entities) {
ValidatingRepositoryEventListener listener = new ValidatingRepositoryEventListener(entities);
configurerDelegate.configureValidatingRepositoryEventListener(listener);
return listener;
}
@Bean
public JpaHelper jpaHelper() {
if (IS_JPA_AVAILABLE) {
return new JpaHelper();
} else {
return null;
}
}
/**
* Main configuration for the REST exporter.
*/
@Bean
public RepositoryRestConfiguration config() {
ProjectionDefinitionConfiguration configuration = new ProjectionDefinitionConfiguration();
// Register projections found in packages
for (Class<?> projection : getProjections(repositories())) {
configuration.addProjection(projection);
}
RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration, metadataConfiguration(),
enumTranslator());
configurerDelegate.configureRepositoryRestConfiguration(config);
return config;
}
@Bean
public static ProjectionDefinitionRegistar projectionDefinitionRegistrar(
ObjectFactory<RepositoryRestConfiguration> config) {
return new ProjectionDefinitionRegistar(config);
}
@Bean
public MetadataConfiguration metadataConfiguration() {
return new MetadataConfiguration();
}
@Bean
public BaseUri baseUri() {
return new BaseUri(config().getBaseUri());
}
/**
* {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as
* {@link org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s.
*
* @return
*/
@Bean
public static AnnotatedEventHandlerInvoker annotatedEventHandlerInvoker() {
return new AnnotatedEventHandlerInvoker();
}
/**
* For merging incoming objects materialized from JSON with existing domain objects loaded from the repository.
*
* @return
* @throws Exception
*/
@Bean
public DomainObjectMerger domainObjectMerger() throws Exception {
return new DomainObjectMerger(repositories(), defaultConversionService());
}
/**
* Turns an {@link javax.servlet.http.HttpServletRequest} into a
* {@link org.springframework.http.server.ServerHttpRequest}.
*
* @return
*/
@Bean
public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() {
return new ServerHttpRequestMethodArgumentResolver();
}
/**
* A convenience resolver that pulls together all the information needed to service a request.
*
* @return
*/
@Bean
public RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver() {
if (QuerydslUtils.QUERY_DSL_PRESENT) {
QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(defaultConversionService(),
factory.getEntityPathResolver());
return new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories(),
repositoryInvokerFactory(defaultConversionService()), resourceMetadataHandlerMethodArgumentResolver(),
predicateBuilder, factory);
}
return new RootResourceInformationHandlerMethodArgumentResolver(repositories(),
repositoryInvokerFactory(defaultConversionService()), resourceMetadataHandlerMethodArgumentResolver());
}
@Bean
public ResourceMetadataHandlerMethodArgumentResolver resourceMetadataHandlerMethodArgumentResolver() {
return new ResourceMetadataHandlerMethodArgumentResolver(repositories(), resourceMappings(), baseUri());
}
@Bean
public BackendIdHandlerMethodArgumentResolver backendIdHandlerMethodArgumentResolver() {
return new BackendIdHandlerMethodArgumentResolver(Java8PluginRegistry.of(backendIdConverterRegistry()),
resourceMetadataHandlerMethodArgumentResolver(), baseUri());
}
@Bean
public ETagArgumentResolver eTagArgumentResolver() {
return new ETagArgumentResolver();
}
/**
* A special {@link org.springframework.hateoas.EntityLinks} implementation that takes repository and current
* configuration into account when generating links.
*
* @return
* @throws Exception
*/
@Bean
public RepositoryEntityLinks entityLinks() {
PagingAndSortingTemplateVariables templateVariables = new ArgumentResolverPagingAndSortingTemplateVariables(
pageableResolver(), sortResolver());
return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), templateVariables,
Java8PluginRegistry.of(backendIdConverterRegistry()));
}
/**
* Reads incoming JSON into an entity.
*
* @return
*/
@Bean
public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() {
return new PersistentEntityResourceHandlerMethodArgumentResolver(defaultMessageConverters(),
repoRequestArgumentResolver(), backendIdHandlerMethodArgumentResolver(),
new DomainObjectReader(persistentEntities(), associationLinks()));
}
/**
* Turns a domain class into a {@link org.springframework.data.rest.webmvc.json.JsonSchema}.
*
* @return
*/
@Bean
public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() {
return new PersistentEntityToJsonSchemaConverter(persistentEntities(), associationLinks(),
resourceDescriptionMessageSourceAccessor(), objectMapper(), config(),
new ValueTypeSchemaPropertyCustomizerFactory(repositoryInvokerFactory(defaultConversionService())));
}
/**
* The {@link MessageSourceAccessor} to provide messages for {@link ResourceDescription}s being rendered.
*
* @return
*/
@Bean
public MessageSourceAccessor resourceDescriptionMessageSourceAccessor() {
try {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("rest-default-messages.properties"));
propertiesFactoryBean.afterPropertiesSet();
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:rest-messages");
messageSource.setCommonMessages(propertiesFactoryBean.getObject());
messageSource.setDefaultEncoding("UTF-8");
return new MessageSourceAccessor(messageSource);
} catch (Exception o_O) {
throw new BeanCreationException("resourceDescriptionMessageSourceAccessor", "", o_O);
}
}
/**
* The Jackson {@link ObjectMapper} used internally.
*
* @return
*/
@Bean
public ObjectMapper objectMapper() {
Jdk8Module jdk8Module = new Jdk8Module();
jdk8Module.configureAbsentsAsNulls(true);
ObjectMapper mapper = basicObjectMapper();
mapper.registerModule(persistentEntityJackson2Module());
mapper.registerModule(jdk8Module);
return mapper;
}
/**
* The {@link HttpMessageConverter} used by Spring MVC to read and write JSON data.
*
* @return
*/
@Bean
public TypeConstrainedMappingJackson2HttpMessageConverter jacksonHttpMessageConverter() {
List<MediaType> mediaTypes = new ArrayList<MediaType>();
// Configure this mapper to be used if HAL is not the default media type
if (!config().useHalAsDefaultJsonMediaType()) {
mediaTypes.add(MediaType.APPLICATION_JSON);
}
int order = config().useHalAsDefaultJsonMediaType() ? Ordered.LOWEST_PRECEDENCE - 1
: Ordered.LOWEST_PRECEDENCE - 10;
mediaTypes.addAll(Arrays.asList(RestMediaTypes.SCHEMA_JSON, //
RestMediaTypes.JSON_PATCH_JSON, RestMediaTypes.MERGE_PATCH_JSON, //
RestMediaTypes.SPRING_DATA_VERBOSE_JSON, RestMediaTypes.SPRING_DATA_COMPACT_JSON));
TypeConstrainedMappingJackson2HttpMessageConverter jacksonConverter = new ResourceSupportHttpMessageConverter(
order);
jacksonConverter.setObjectMapper(objectMapper());
jacksonConverter.setSupportedMediaTypes(mediaTypes);
return jacksonConverter;
}
//
// HAL setup
//
@Bean
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter() {
ArrayList<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(MediaTypes.HAL_JSON);
// Enable returning HAL if application/json is asked if it's configured to be the default type
if (config().useHalAsDefaultJsonMediaType()) {
mediaTypes.add(MediaType.APPLICATION_JSON);
}
int order = config().useHalAsDefaultJsonMediaType() ? Ordered.LOWEST_PRECEDENCE - 10
: Ordered.LOWEST_PRECEDENCE - 1;
TypeConstrainedMappingJackson2HttpMessageConverter converter = new ResourceSupportHttpMessageConverter(order);
converter.setObjectMapper(halObjectMapper());
converter.setSupportedMediaTypes(mediaTypes);
return converter;
}
@Bean
public ObjectMapper halObjectMapper() {
RelProvider defaultedRelProvider = this.relProvider != null ? this.relProvider : new EvoInflectorRelProvider();
HalHandlerInstantiator instantiator = new HalHandlerInstantiator(defaultedRelProvider, curieProvider,
resourceDescriptionMessageSourceAccessor(), applicationContext.getAutowireCapableBeanFactory());
ObjectMapper mapper = basicObjectMapper();
mapper.registerModule(persistentEntityJackson2Module());
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(instantiator);
return mapper;
}
/**
* The {@link HttpMessageConverter} used to create {@literal text/uri-list} responses.
*
* @return
*/
@Bean
public UriListHttpMessageConverter uriListHttpMessageConverter() {
return new UriListHttpMessageConverter();
}
@Bean
@SuppressWarnings("rawtypes")
public ResourceProcessorInvoker resourceProcessorInvoker() {
Collection<ResourceProcessor> beans = applicationContext.getBeansOfType(ResourceProcessor.class, false, false)
.values();
List<ResourceProcessor<?>> processors = new ArrayList<ResourceProcessor<?>>(beans.size());
for (ResourceProcessor<?> bean : beans) {
processors.add(bean);
}
return new ResourceProcessorInvoker(processors);
}
/**
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the
* provided controller classes.
*
* @param resourceProcessors {@link ResourceProcessor}s available in the {@link ApplicationContext}.
* @return
*/
@Bean
public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
// Forward conversion service to handler adapter
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(defaultConversionService());
RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
resourceProcessorInvoker());
handlerAdapter.setWebBindingInitializer(initializer);
handlerAdapter.setMessageConverters(defaultMessageConverters());
if (config().getMetadataConfiguration().alpsEnabled()) {
handlerAdapter.setResponseBodyAdvice(Arrays.<ResponseBodyAdvice<?>> asList(alpsJsonHttpMessageConverter()));
}
return handlerAdapter;
}
/**
* {@link HttpRequestHandlerAdapter} to handle CORS preflight requests.
*
* @return
*/
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}
/**
* The {@link HandlerMapping} to delegate requests to Spring Data REST controllers. Sets up a
* {@link DelegatingHandlerMapping} to make sure manually implemented {@link BasePathAwareController} instances that
* register custom handlers for certain media types don't cause the {@link RepositoryRestHandlerMapping} to be
* omitted. See DATAREST-490.
*
* @return
*/
@Bean
public DelegatingHandlerMapping restHandlerMapping() {
Map<String, CorsConfiguration> corsConfigurations = config().getCorsRegistry().getCorsConfigurations();
RepositoryRestHandlerMapping repositoryMapping = new RepositoryRestHandlerMapping(resourceMappings(), config(),
repositories());
repositoryMapping.setJpaHelper(jpaHelper());
repositoryMapping.setApplicationContext(applicationContext);
repositoryMapping.setCorsConfigurations(corsConfigurations);
repositoryMapping.afterPropertiesSet();
BasePathAwareHandlerMapping basePathMapping = new BasePathAwareHandlerMapping(config());
basePathMapping.setApplicationContext(applicationContext);
basePathMapping.setCorsConfigurations(corsConfigurations);
basePathMapping.afterPropertiesSet();
List<HandlerMapping> mappings = new ArrayList<HandlerMapping>();
mappings.add(basePathMapping);
mappings.add(repositoryMapping);
return new DelegatingHandlerMapping(mappings);
}
@Bean
public RepositoryResourceMappings resourceMappings() {
return new RepositoryResourceMappings(repositories(), persistentEntities(),
config().getRepositoryDetectionStrategy());
}
/**
* Jackson module responsible for intelligently serializing and deserializing JSON that corresponds to an entity.
*
* @return
*/
protected Module persistentEntityJackson2Module() {
PersistentEntities entities = persistentEntities();
ConversionService conversionService = defaultConversionService();
UriToEntityConverter uriToEntityConverter = uriToEntityConverter(conversionService);
RepositoryInvokerFactory repositoryInvokerFactory = repositoryInvokerFactory(conversionService);
EmbeddedResourcesAssembler assembler = new EmbeddedResourcesAssembler(entities, associationLinks(),
excerptProjector());
LookupObjectSerializer lookupObjectSerializer = new LookupObjectSerializer(
Java8PluginRegistry.of(getEntityLookups()));
return new PersistentEntityJackson2Module(associationLinks(), entities, uriToEntityConverter, linkCollector(),
repositoryInvokerFactory, lookupObjectSerializer, resourceProcessorInvoker(), assembler);
}
@Bean
protected LinkCollector linkCollector() {
return new LinkCollector(persistentEntities(), selfLinkProvider(), associationLinks());
}
protected UriToEntityConverter uriToEntityConverter(ConversionService conversionService) {
return new UriToEntityConverter(persistentEntities(), repositoryInvokerFactory(conversionService), repositories());
}
@Bean
public ExcerptProjector excerptProjector() {
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
projectionFactory.setBeanFactory(applicationContext);
return new DefaultExcerptProjector(projectionFactory, resourceMappings());
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#extendHandlerExceptionResolvers(java.util.List)
*/
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver();
er.setCustomArgumentResolvers(defaultMethodArgumentResolvers());
er.setMessageConverters(defaultMessageConverters());
configurerDelegate.configureExceptionHandlerExceptionResolver(er);
exceptionResolvers.add(0, er);
}
@Bean
public RepositoryRestExceptionHandler repositoryRestExceptionHandler() {
return new RepositoryRestExceptionHandler(applicationContext);
}
@Bean
public RepositoryInvokerFactory repositoryInvokerFactory(@Qualifier ConversionService defaultConversionService) {
return new UnwrappingRepositoryInvokerFactory(
new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService), getEntityLookups());
}
@Bean
public List<HttpMessageConverter<?>> defaultMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
if (config().getMetadataConfiguration().alpsEnabled()) {
messageConverters.add(alpsJsonHttpMessageConverter());
}
if (config().getDefaultMediaType().equals(MediaTypes.HAL_JSON)) {
messageConverters.add(halJacksonHttpMessageConverter());
messageConverters.add(jacksonHttpMessageConverter());
} else {
messageConverters.add(jacksonHttpMessageConverter());
messageConverters.add(halJacksonHttpMessageConverter());
}
MappingJackson2HttpMessageConverter fallbackJsonConverter = new MappingJackson2HttpMessageConverter();
fallbackJsonConverter.setObjectMapper(basicObjectMapper());
messageConverters.add(fallbackJsonConverter);
messageConverters.add(uriListHttpMessageConverter());
configurerDelegate.configureHttpMessageConverters(messageConverters);
return messageConverters;
}
@Bean
public AlpsJsonHttpMessageConverter alpsJsonHttpMessageConverter() {
return new AlpsJsonHttpMessageConverter(alpsConverter());
}
/*
* (non-Javadoc)
* @see org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration#pageableResolver()
*/
@Bean
@Override
public HateoasPageableHandlerMethodArgumentResolver pageableResolver() {
HateoasPageableHandlerMethodArgumentResolver resolver = super.pageableResolver();
resolver.setPageParameterName(config().getPageParamName());
resolver.setSizeParameterName(config().getLimitParamName());
resolver.setFallbackPageable(PageRequest.of(0, config().getDefaultPageSize()));
resolver.setMaxPageSize(config().getMaxPageSize());
return resolver;
}
/*
* (non-Javadoc)
* @see org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration#sortResolver()
*/
@Bean
@Override
public HateoasSortHandlerMethodArgumentResolver sortResolver() {
HateoasSortHandlerMethodArgumentResolver resolver = super.sortResolver();
resolver.setSortParameter(config().getSortParamName());
return resolver;
}
@Bean
public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() {
List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(idConverters.size());
converters.addAll(this.idConverters);
converters.add(DefaultIdConverter.INSTANCE);
return OrderAwarePluginRegistry.create(converters);
}
@Bean
public AuditableBeanWrapperFactory auditableBeanWrapperFactory() {
return new MappingAuditableBeanWrapperFactory(persistentEntities());
}
@Bean
public HttpHeadersPreparer httpHeadersPreparer() {
return new HttpHeadersPreparer(auditableBeanWrapperFactory());
}
@Bean
public SelfLinkProvider selfLinkProvider() {
return new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), getEntityLookups());
}
@Bean
public Associations associationLinks() {
return new Associations(resourceMappings(), config());
}
protected List<EntityLookup<?>> getEntityLookups() {
List<EntityLookup<?>> lookups = new ArrayList<EntityLookup<?>>();
lookups.addAll(config().getEntityLookups(repositories()));
lookups.addAll(this.lookups);
return lookups;
}
protected List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
projectionFactory.setBeanFactory(applicationContext);
projectionFactory.setResourceLoader(applicationContext);
PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver(
persistentEntities(), selfLinkProvider(), config().getProjectionConfiguration(), projectionFactory,
associationLinks());
PageableHandlerMethodArgumentResolver pageableResolver = pageableResolver();
JacksonMappingAwareSortTranslator sortTranslator = new JacksonMappingAwareSortTranslator(objectMapper(),
repositories(), DomainClassResolver.of(repositories(), resourceMappings(), baseUri()), persistentEntities(),
associationLinks());
HandlerMethodArgumentResolver sortResolver = new MappingAwareSortArgumentResolver(sortTranslator, sortResolver());
HandlerMethodArgumentResolver jacksonPageableResolver = new MappingAwarePageableArgumentResolver(sortTranslator,
pageableResolver);
HandlerMethodArgumentResolver defaultedPageableResolver = new MappingAwareDefaultedPageableArgumentResolver(
sortTranslator, pageableResolver);
return Arrays.asList(defaultedPageableResolver, jacksonPageableResolver, sortResolver,
serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, peraResolver,
backendIdHandlerMethodArgumentResolver(), eTagArgumentResolver());
}
@Autowired GeoModule geoModule;
protected ObjectMapper basicObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Configure custom Modules
configurerDelegate.configureJacksonObjectMapper(objectMapper);
objectMapper.registerModule(geoModule);
if (config().isEnableEnumTranslation()) {
objectMapper.registerModule(new JacksonSerializers(enumTranslator()));
}
Jackson2DatatypeHelper.configureObjectMapper(objectMapper);
return objectMapper;
}
@Bean
public EnumTranslator enumTranslator() {
return new EnumTranslator(resourceDescriptionMessageSourceAccessor());
}
@SuppressWarnings("unchecked")
private Set<Class<?>> getProjections(Repositories repositories) {
Set<String> packagesToScan = new HashSet<String>();
for (Class<?> domainType : repositories) {
packagesToScan.add(domainType.getPackage().getName());
}
AnnotatedTypeScanner scanner = new AnnotatedTypeScanner(Projection.class);
scanner.setEnvironment(applicationContext.getEnvironment());
scanner.setResourceLoader(applicationContext);
return scanner.findTypes(packagesToScan);
}
//
// ALPS support
//
@Bean
public RootResourceInformationToAlpsDescriptorConverter alpsConverter() {
Repositories repositories = repositories();
PersistentEntities persistentEntities = persistentEntities();
RepositoryEntityLinks entityLinks = entityLinks();
MessageSourceAccessor messageSourceAccessor = resourceDescriptionMessageSourceAccessor();
RepositoryRestConfiguration config = config();
return new RootResourceInformationToAlpsDescriptorConverter(associationLinks(), repositories, persistentEntities,
entityLinks, messageSourceAccessor, config, objectMapper(), enumTranslator());
}
@Bean
public ProfileResourceProcessor profileResourceProcessor(RepositoryRestConfiguration config) {
return new ProfileResourceProcessor(config);
}
//
// HAL Browser
//
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#addResourceHandlers(org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry)
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Register HAL browser if present
if (ClassUtils.isPresent("org.springframework.data.rest.webmvc.halbrowser.HalBrowser",
getClass().getClassLoader())) {
String basePath = config().getBasePath().toString().concat("/browser");
String rootLocation = "classpath:META-INF/spring-data-rest/hal-browser/";
registry.addResourceHandler(basePath.concat("/**")).addResourceLocations(rootLocation);
}
}
private static class ResourceSupportHttpMessageConverter extends TypeConstrainedMappingJackson2HttpMessageConverter
implements Ordered {
private final int order;
/**
* Creates a new {@link ResourceSupportHttpMessageConverter} with the given order.
*
* @param order the order for the {@link HttpMessageConverter}.
*/
public ResourceSupportHttpMessageConverter(int order) {
super(ResourceSupport.class);
this.order = order;
}
/*
* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
public int getOrder() {
return order;
}
}
}